Skip to content

perf(repsel): element-shape versioned loop clone — the first consumer of the element-shape invariant (#7480 / #5093) - #7612

Merged
proggeramlug merged 2 commits into
mainfrom
perf/5093-element-shape-loop-clone
Aug 8, 2026
Merged

perf(repsel): element-shape versioned loop clone — the first consumer of the element-shape invariant (#7480 / #5093)#7612
proggeramlug merged 2 commits into
mainfrom
perf/5093-element-shape-loop-clone

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The consumer half of #7480; continues #5093's versioned-loop line.

#7496 landed the per-array homogeneous element-shape invariant with no
consumer, on purpose
. This is the consumer.

for (let j = 0; j < n; j++) sum += keep[j].v now gets a specialized clone
behind a preheader guard on "this array holds the element-shape invariant at
class C". Inside the clone the element read is a bare gep + load off a
preheader-cached elements base and the field read is a bare raw-f64 slot load.
The existing generic body survives unchanged as the cold arm.

kernel (200k elements × 50 sweeps, best-of-9 interleaved, checksums equal) before after node speedup
keep: Node[], sum += keep[j].v 41 ms 13 ms 13 ms 3.15× — parity with node, from 3.15× behind
keep: {v,w}[] (object literal) 86 ms 86 ms 10 ms 1.00× — deliberately out of scope

Revocation mechanism, and its failure mode

Restrict-the-body, enforced twice — the second enforcement is the
load-bearing one.

  1. By shape (the matcher). A single store-free acc = <pure numeric>
    statement over arr[counter].field reads, numeric locals, numeric literals
    and pure arithmetic / Math. No stores, calls, closures, await, or
    updates other than the counter's. No catch-all arm.
  2. By construction (the lowering). After the fast clone is emitted, every
    one of its blocks is scanned for a GC-unsafe call. If any survived, the
    deref block branches unconditionally to the slow clone and the fast
    blocks are left as unreachable code.

Call-freeness is exactly the right property, because every way to revoke the
invariant is a runtime call — element store (layout_note_slot
note_element_store), length change (caught by the record's pinned
verified_len), delete (a TAG_HOLE store through the same funnel),
defineProperty on the array, prototype surgery — and so is every allocation
that could move the array. Codegen's inline element store is the one path
that can skip the note, and only when the array is statically proven numeric
and pointer-free, which an element-shape array can never be.

Failure mode: conservative, never unsound. Anything that writes, calls, or
reads a field the analysis cannot type gets no clone and runs exactly as
today. The residual risk is a silent loss of the optimization, which is why
the codegen tests assert the fast blocks appear in the emitted IR and that
the fast clone contains no call at all.

A useful consequence of the same rule, verified in the emitted IR: under
PERRY_GC_MOVING_LOOP_POLLS=1 the back-edge safepoint is itself a call, so the
scan fails and the deref block emits an unconditional
br label %element_shape.loop.slow.preheader. The clone stands down in exactly
the configuration where a mid-loop collection could move the array — with no
special case for it anywhere in the code.

The guard tests the live header; the brand is explicit

The preheader calls js_array_ensure_element_shape#7496's own query
surface, which reads the array's current GcHeader bit and record and
self-heals when the record went stale. No inline reimplementation, so no drift
(#7501).

Sequencing is load-bearing. The GC_TYPE_ARRAY brand test comes first, so
the pointer handed to the runtime is already branded — an Array subclass
instance is a plain ObjectHeader whose fields overlay ArrayHeader's
(#7573/#7603). The elements base pointer is derived only after the guard
call returns, from a fresh load of the array's rooted slot: the call can
allocate, and an allocation can move the array.

What the invariant does not prove

element_class_of_bits proves POINTER_TAG, a readable GcHeader,
GC_TYPE_OBJECT, OBJECT_TYPE_REGULAR and class_id == C for every element
in the verified prefix — exactly the predicates the element-read tier and the
front half of the field-read precheck spend per iteration, so the clone drops
them. It proves nothing about the per-object facts a raw-f64 slot load needs
(keys_array identity — delete elem.f compacts the packed slots while
preserving class_id — plus field_count, the descriptor flag, and the
typed-layout intact bit). Dropping those would be the miscompile, so the clone
keeps a residual per-element check, collapsed to one 4-byte load of the three
contiguous header bytes plus two more loads and a single side-exit branch.
Emitted fast body: zero calls, one branch, no volatile gate load.

Folding those facts into the invariant is the documented next slice and needs
an invalidation surface for delete / defineProperty / typed downgrade that
#7496 deliberately did not open. It should land the way #7496 did: invariant
first, matrix second, consumer third.

Sabotage, both directions

arm clone blocks in IR js_array_ensure_element_shape calls gap test
base (main) 0 0 identical to node
this 1 of each 1 identical to node
sabotage: never-specialize 0 0 identical to node
sabotage: always-specialize 1 of each 1 SIGBUS, exit 138

Breaking the guard (every shape fact discarded, per-element check never
side-exits) faults at the subclass: case — #7603's ObjectHeader-read-as-
ArrayHeader reproduced on demand. Breaking the clone selection leaves the
gap test byte-identical to node while the census drops to the same zero the
base compiler emits, so the fallback is behaviour-neutral and the census is
measuring the clone rather than something incidental.

Size (#7566's discipline)

Both recorded traps avoided — runtime trip counts so nothing unrolls, arrays
escaping through console.log so nothing is scalar-replaced. Measured on the
module object file so the runtime archive does not blur it.

probe base this delta
40 loops, none qualifying 83,000 B 83,000 B +0 B, object bytes byte-identical
40 loops, all qualifying 93,904 B 108,872 B +14,968 B (+15.9%, ~374 B/loop)

Runtime archive +232 B for the single keepalive-anchors static — exactly one,
for the one symbol codegen now emits a call to; the other four
js_array_element_shape_* entry points stay unanchored and dead-strippable.

Verification

  • Gap test byte-identical to node, exit 0
    test-files/test_gap_repsel_element_shape_loop_clone.ts covers the hot
    shape plus mid-loop store revocation (direct and via a call), revocation
    between two entries of the same loop, subclass receiver (const and
    plainly-typed parameter), shape-mismatched and heterogeneous arrays, holes /
    sparse / delete, empty array, per-element typed-layout downgrade, deleted
    field, own accessor, frozen element, prototype surgery, every length
    mutation, and a bound past the array's length.
  • Gap-suite family A/B (array / object / class / repsel / new /
    prop, 71 tests) against a same-session main build: verdict sets
    identical
    — 70 PASS / 1 FAIL on both arms, the failure
    (test_gap_prop_plan_cache_invalidation) pre-existing on both. This is
    perf(codegen): keep the numeric-array specialization when the array is captured (#6369) #6377's gate.
  • GC root-dominance, curated corpus (129/129 sources compiled, 149
    modules), both gated modes: --moving-only 0 violations with 40/40 seeded
    violations caught
    ; --unrooted-allocas --moving-only 0 violations over
    7,860 GC-capable allocas. Allowlist empty and stays empty.
  • cargo test -p perry-runtime --no-fail-fast 1880 passed / 0 failed;
    cargo test -p perry-codegen --lib 685 passed / 0 failed (7 new).
  • cargo fmt --all --check, file-size cap, addr-class ratchet, GC store-site
    inventory, workspace-architecture policy: all clean.

Scope

Declared element types (keep: Node[]) only. #7480's own object-literal kernel
(keep: {v,w}[]) is deliberately out: receiver_class_name returning None
for an Object-typed element is also what makes the number-context field-read
helper decline, so a wider matcher would buy only dead fast-clone IR. Reaching
it means teaching static_type_of / receiver_class_name to type an
Object-typed property read — the #6377 "more type visibility un-gates latent
fast paths" change, which needs its own gap-suite A/B. Element classes with a
base class are also declined: an inherited layout is not described by the
packed slot index alone, and a native base (extends Array) is the
#7573/#7603 hazard itself.

https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix

Summary by CodeRabbit

  • New Features

    • Added an optimized execution path for numeric loops over compatible homogeneous object arrays.
    • Added automatic safety checks that fall back to standard loop behavior when array layouts, contents, mutations, or other conditions are unsupported.
  • Bug Fixes

    • Improved handling of array shape changes, inheritance, sparse or heterogeneous arrays, accessors, and runtime mutations.
  • Tests

    • Added comprehensive coverage for optimized loops, fallback scenarios, bounds changes, and invalidated assumptions.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d01e29f1-c54a-4723-acfc-a4f93a0a5429

📥 Commits

Reviewing files that changed from the base of the PR and between 43e5ae7 and 1dd3bd8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • CLAUDE.md
  • Cargo.toml

📝 Walkthrough

Walkthrough

Adds guarded element-shape loop cloning for eligible numeric Node[] loops. The fast clone uses cached element data and raw field loads. Generic lowering remains the fallback when validation fails or unsafe calls are present.

Changes

Element-shape loop cloning

Layer / File(s) Summary
Loop context and runtime contracts
crates/perry-codegen/src/expr/mod.rs, crates/perry-codegen/src/runtime_decls/arrays.rs, crates/perry-codegen/src/codegen/*, crates/perry-runtime/src/array/element_shape.rs, crates/perry-codegen/src/stmt/mod.rs
FnCtx stores scoped ElementShapeLoopFact entries. Code generation initializes the collection. The runtime declaration and keepalive anchor expose js_array_ensure_element_shape.
Loop matching and lowering dispatch
crates/perry-codegen/src/stmt/element_shape_loop.rs, crates/perry-codegen/src/stmt/loops.rs
The matcher accepts restricted numeric expressions and validates bounds, layouts, classes, accessors, inheritance, and denylisted cases. lower_for invokes versioned lowering before generic lowering.
Guarded field access
crates/perry-codegen/src/expr/element_shape_guard.rs, crates/perry-codegen/src/expr/property_get/helpers.rs, crates/perry-codegen/src/stmt/element_shape_loop.rs
The fast clone checks array branding, element shape, length, headers, field counts, keys, and typed layout. Matching indexed property reads use guarded raw double field loads. Unsafe calls disable the fast path.
Clone validation and observable coverage
crates/perry-codegen/src/stmt/element_shape_loop_tests.rs, test-files/test_gap_repsel_element_shape_loop_clone.ts, changelog.d/7612-element-shape-loop-clone.md, Cargo.toml, CLAUDE.md
Tests verify clone emission, fallback behavior, constant assumptions, mutations, sparse arrays, subclasses, layout hazards, prototype changes, and bounds. The changelog records implementation and verification results. Package documentation and workspace metadata use version 0.5.1350.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant lower_for
  participant element_shape_loop
  participant element_shape_guard
  participant js_array_ensure_element_shape
  participant property_get_helpers
  lower_for->>element_shape_loop: match and lower eligible loop
  element_shape_loop->>element_shape_guard: emit preheader checks
  element_shape_guard->>js_array_ensure_element_shape: validate array element shape
  js_array_ensure_element_shape-->>element_shape_guard: return shape class
  element_shape_loop->>property_get_helpers: lower arr[i].field
  property_get_helpers->>element_shape_guard: emit residual checks and raw field load
Loading

Possibly related PRs

  • PerryTS/perry#7496: Provides the element-shape invariant and runtime API used by this specialization.
  • PerryTS/perry#6810: Uses shared loop-specialization code for a separate numeric loop optimization.
  • PerryTS/perry#6911: Uses related shape and layout proofs for fixed-offset numeric field loads.

Suggested labels: run-extended-tests

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly covers the change, related issues, implementation details, scope, and extensive verification results, despite not following the template headings exactly.
Title check ✅ Passed The title clearly identifies the performance optimization and its role as the first consumer of the element-shape invariant.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/5093-element-shape-loop-clone

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The first consumer of the per-array homogeneous element-shape invariant
(#7496, matrix #7608), which landed with no consumer on purpose.

`for (let j = 0; j < n; j++) sum += keep[j].v` gets a specialized clone
behind a preheader guard on "this array holds the element-shape invariant
at class C". The element read becomes a bare gep+load off a cached
elements base and the field read a bare raw-f64 slot load; the generic
body survives unchanged as the cold arm. Measured 41ms -> 13ms (3.15x),
now at parity with node, at +0 bytes on a program with no qualifying loop.

Revocation mechanism: restrict-the-body, enforced twice — by shape in the
matcher (a single store-free `acc = <pure numeric>` statement) and by
construction in the lowering, which scans every emitted block of the fast
clone for a GC-unsafe call and branches unconditionally to the slow clone
if one survived. Call-freeness is exactly the right property: every way to
revoke the invariant (element store, length change, delete, defineProperty,
prototype surgery) is a runtime call, and so is every allocation that could
move the array. Failure mode: conservative, never unsound.

Claude-Session: https://claude.ai/code/session_01Y1QZ5wUP9gRSwpiweT4Wix

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
crates/perry-codegen/src/expr/element_shape_guard.rs (1)

248-254: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Derive the field_count and keys_array ObjectHeader offsets, or assert them in constants_match_the_runtime.

object_header_size_bytes comes from crate::target_layout, and the module drift-tests every mirrored runtime mask constant. The +12 and +16 ObjectHeader field offsets are the same target-dependent layout fact, but they are inline literals. If ObjectHeader changes, these reads can target the wrong bytes.

Reuse derived offset constants, or add ObjectHeader::field_count and ObjectHeader::keys_array checks to constants_match_the_runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/expr/element_shape_guard.rs` around lines 248 - 254,
The ObjectHeader reads in the guard use unvalidated inline offsets. Replace the
`12` and `16` literals in the `field_count` and `keys_array` GEPs with
target-layout-derived offset constants, or extend `constants_match_the_runtime`
with checks for `ObjectHeader::field_count` and `ObjectHeader::keys_array` so
these offsets remain synchronized with the runtime.
crates/perry-codegen/src/stmt/element_shape_loop_tests.rs (1)

272-293: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The store test declines on statement count, not on the store.

The body here has two statements. The matcher requires exactly one Stmt::Expr(Expr::LocalSet(..)) body statement, so it declines at the slice pattern and never inspects the store. The test name and the doc comment claim store detection is the reason.

The assertion is still correct, but it does not pin the store rule. Consider adding a case whose body is a single statement that writes, for example Stmt::Expr(Expr::IndexSet { .. }) alone, so a future widening of the body shape to multiple statements still fails here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/stmt/element_shape_loop_tests.rs` around lines 272 -
293, Add a separate test for the element-shape versioned loop using a
single-statement body containing only Expr::IndexSet, so rejection is
specifically exercised by store detection rather than statement-count matching.
Keep the existing assertion that no CLONE_LABELS are emitted and retain the
current multi-statement case only if it covers a distinct behavior.
crates/perry-codegen/src/stmt/element_shape_loop.rs (1)

553-560: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Record that the deref block is outside the call-free scan.

fast_scan_start is read after emit_element_shape_loop_preheader_check returns, so deref_idx sits below the scanned range and is never checked. The SAFETY comment at Line 435 states the call-free window starts at the post-guard re-derivation, which includes that block.

The code is safe today because the helper derives elements_base as the last pointer computation in deref_idx and emits only loads afterwards. A future edit to that helper could add a call after the derivation and no scan would catch it. Consider scanning deref_idx as well, or stating the exclusion at this site.

🛡️ Proposed scan widening
-    let fast_clone_call_free = !ctx.func.blocks()[fast_pre_idx].contains_gc_unsafe_call()
+    let fast_clone_call_free = !ctx.func.blocks()[deref_idx].contains_gc_unsafe_call()
+        && !ctx.func.blocks()[fast_pre_idx].contains_gc_unsafe_call()
         && (fast_scan_start..fast_scan_end)
             .all(|idx| !ctx.func.blocks()[idx].contains_gc_unsafe_call());

Note: the guard call itself lives in query_idx, not deref_idx, so this does not disable the clone.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/stmt/element_shape_loop.rs` around lines 553 - 560,
Include the dereference block identified by deref_idx in the
fast_clone_call_free verification, since the call-free window begins there but
the current range starts at fast_scan_start. Preserve the existing fast_pre_idx
and subsequent-block checks while widening the scan to cover deref_idx, or
explicitly document and enforce its exclusion if that is required.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-codegen/src/expr/element_shape_guard.rs`:
- Around line 280-335: Update constants_match_the_runtime to derive forwarded,
has_descriptors, typed_intact, and the expected object type from the
corresponding runtime constants rather than local literals. Expose
GC_FLAG_FORWARDED, OBJ_FLAG_HAS_DESCRIPTORS, GC_OBJ_TYPED_LAYOUT_INTACT, and
GC_TYPE_OBJECT through perry-codegen’s public API, then use those exported
symbols when constructing mask and expect so runtime changes are detected.

---

Nitpick comments:
In `@crates/perry-codegen/src/expr/element_shape_guard.rs`:
- Around line 248-254: The ObjectHeader reads in the guard use unvalidated
inline offsets. Replace the `12` and `16` literals in the `field_count` and
`keys_array` GEPs with target-layout-derived offset constants, or extend
`constants_match_the_runtime` with checks for `ObjectHeader::field_count` and
`ObjectHeader::keys_array` so these offsets remain synchronized with the
runtime.

In `@crates/perry-codegen/src/stmt/element_shape_loop_tests.rs`:
- Around line 272-293: Add a separate test for the element-shape versioned loop
using a single-statement body containing only Expr::IndexSet, so rejection is
specifically exercised by store detection rather than statement-count matching.
Keep the existing assertion that no CLONE_LABELS are emitted and retain the
current multi-statement case only if it covers a distinct behavior.

In `@crates/perry-codegen/src/stmt/element_shape_loop.rs`:
- Around line 553-560: Include the dereference block identified by deref_idx in
the fast_clone_call_free verification, since the call-free window begins there
but the current range starts at fast_scan_start. Preserve the existing
fast_pre_idx and subsequent-block checks while widening the scan to cover
deref_idx, or explicitly document and enforce its exclusion if that is required.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1992faae-beb5-4b8c-a27e-453adbc392b2

📥 Commits

Reviewing files that changed from the base of the PR and between 9caa11a and 43e5ae7.

📒 Files selected for processing (15)
  • changelog.d/7612-element-shape-loop-clone.md
  • crates/perry-codegen/src/codegen/closure.rs
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/codegen/function.rs
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/expr/element_shape_guard.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/property_get/helpers.rs
  • crates/perry-codegen/src/runtime_decls/arrays.rs
  • crates/perry-codegen/src/stmt/element_shape_loop.rs
  • crates/perry-codegen/src/stmt/element_shape_loop_tests.rs
  • crates/perry-codegen/src/stmt/loops.rs
  • crates/perry-codegen/src/stmt/mod.rs
  • crates/perry-runtime/src/array/element_shape.rs
  • test-files/test_gap_repsel_element_shape_loop_clone.ts

Comment on lines +280 to +335
#[test]
fn constants_match_the_runtime() {
assert_eq!(POINTER_TAG_HI16, (0x7FFDu64).to_string());
assert_eq!(HANDLE_BAND_TOP, (0x0F_FFFFu64).to_string());
assert_eq!(GC_TYPE_ARRAY, "1");

// Reconstruct the header mask from the individual runtime constants,
// positioned by their byte offsets inside the i32 at `obj - 8`.
let obj_type_mask = 0x0000_00FFu32;
let forwarded = u32::from(0x80u8) << 8; // GC_FLAG_FORWARDED @ -7
let has_descriptors = 0x0800u32 << 16; // OBJ_FLAG_HAS_DESCRIPTORS @ -6
let typed_intact = 0x1000u32 << 16; // GC_OBJ_TYPED_LAYOUT_INTACT @ -6
let mask = obj_type_mask | forwarded | has_descriptors | typed_intact;
let expect = u32::from(2u8) /* GC_TYPE_OBJECT */ | typed_intact;

assert_eq!(ELEM_HEADER_MASK, mask.to_string(), "header mask drifted");
assert_eq!(
ELEM_HEADER_EXPECT,
expect.to_string(),
"header expectation drifted"
);

// Sabotage direction: the mask must actually reject each fact.
let good = expect;
assert_eq!(good & mask, expect);
assert_ne!((good | forwarded) & mask, expect, "forwarded not rejected");
assert_ne!(
(good | has_descriptors) & mask,
expect,
"descriptors not rejected"
);
assert_ne!(
(good & !typed_intact) & mask,
expect,
"typed-layout downgrade not rejected"
);
assert_ne!((good ^ 1) & mask, expect, "wrong obj_type not rejected");
}

/// The mask reads three adjacent header bytes as one little-endian i32.
/// Perry emits for aarch64/x86_64 only; assert the assumption explicitly
/// so a future big-endian target trips here rather than in a field load.
#[test]
fn header_word_assumes_little_endian_targets() {
for triple in [
"aarch64-apple-darwin",
"x86_64-unknown-linux-gnu",
"aarch64-linux-android",
"x86_64-pc-windows-msvc",
] {
assert!(
!triple.starts_with("s390") && !triple.starts_with("powerpc64-"),
"{triple} is big-endian; ELEM_HEADER_MASK byte positions are LE-only"
);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Are the runtime header constants public and reachable from perry-codegen?
rg -nP -C2 '\b(GC_FLAG_FORWARDED|OBJ_FLAG_HAS_DESCRIPTORS|GC_OBJ_TYPED_LAYOUT_INTACT|GC_TYPE_OBJECT|GC_TYPE_ARRAY)\b\s*[:=]' crates/perry-runtime/src

# Does perry-codegen already depend on perry-runtime?
fd -t f 'Cargo.toml' crates/perry-codegen --exec rg -n -C3 'perry-runtime|\[dependencies\]' {}

# Is there an authoritative supported-target-triple list to assert against?
rg -nP -C3 'target_triple|supported_targets|is_macos_triple' crates/perry-codegen/src/target_layout.rs

Repository: PerryTS/perry

Length of output: 26750


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== element_shape_guard constants and tests =="
sed -n '1,140p;240,340p' crates/perry-codegen/src/expr/element_shape_guard.rs

echo "== perry-codegen dependency and feature exports =="
cat crates/perry-codegen/Cargo.toml
printf '\n-- perry-api-manifest exports/publish related lines --\n'
sed -n '1,120p' crates/perry-api-manifest/Cargo.toml 2>/dev/null || true

echo "== constants declaration context =="
sed -n '1,90p' crates/perry-runtime/src/gc/layout.rs
sed -n '12,24p' crates/perry-runtime/src/gc/types.rs
sed -n '910,920p' crates/perry-runtime/src/gc/types.rs
sed -n '978,984p' crates/perry-runtime/src/gc/types.rs

echo "== deterministic local-test failure model =="
python3 - <<'PY'
import re
from pathlib import Path
text = Path('crates/perry-codegen/src/expr/element_shape_guard.rs').read_text()
m = re.search(r'fn constants_match_the_runtime\(\)[\s\S]*?\n    \}\n?(\n    \/\*|\n    #\[test\])', text)
print("FUNCTION_HAS_CONST_TEST:", m is not None)
if m:
    body = m.group(0)
    # local literals used only within the test: check if runtime names appear
    literals_used = bool(re.search(r'\b(0x80|0x0800|0x1000|GC_TYPE_OBJECT|2)\s*=', body))
    print("FUNCTION_USES_LOCAL_LITERAL_MASK_PARTS:", literals_used)
    print("FUNCTION_CONTAINS_RUNTIME_CONST_NAME_FLAGS:", 'GC_FLAG_FORWARDED' in body)
    print("FUNCTION_CONTAINS_RUNTIME_CONST_NAME_DESRIPTORS:", 'OBJ_FLAG_HAS_DESCRIPTORS' in body)
    print("FUNCTION_CONTAINS_RUNTIME_CONST_NAME_INTACT:", 'GC_OBJ_TYPED_LAYOUT_INTACT' in body)
    print("FUNCTION_CONTAINS_RUNTIME_CONST_NAME_ARRAY_TYPE:", 'GC_TYPE_ARRAY' in body)
PY

echo "== supported triple references =="
rg -n -C 2 'aarch64-apple-darwin|x86_64-unknown-linux-gnu|target_endian|little_endian|supported|triple' crates/perry-codegen/src crates/perry-api-manifest/src 2>/dev/null | head -200

Repository: PerryTS/perry

Length of output: 36311


Compare the header constants to the runtime source.

constants_match_the_runtime currently builds mask from local literals (0x80, 0x0800, 0x1000) and expect from 2. If GC_FLAG_FORWARDED, OBJ_FLAG_HAS_DESCRIPTORS, GC_OBJ_TYPED_LAYOUT_INTACT, or GC_TYPE_OBJECT changes at runtime, this test still passes while ELEM_HEADER_MASK and ELEM_HEADER_EXPECT stay wrong. Add the runtime constants to perry-codegen’s public API and compute the constants from those values instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/expr/element_shape_guard.rs` around lines 280 - 335,
Update constants_match_the_runtime to derive forwarded, has_descriptors,
typed_intact, and the expected object type from the corresponding runtime
constants rather than local literals. Expose GC_FLAG_FORWARDED,
OBJ_FLAG_HAS_DESCRIPTORS, GC_OBJ_TYPED_LAYOUT_INTACT, and GC_TYPE_OBJECT through
perry-codegen’s public API, then use those exported symbols when constructing
mask and expect so runtime changes are detected.

@proggeramlug
proggeramlug force-pushed the perf/5093-element-shape-loop-clone branch from 43e5ae7 to 1dd3bd8 Compare August 8, 2026 00:18
@proggeramlug
proggeramlug merged commit 96e0348 into main Aug 8, 2026
@proggeramlug
proggeramlug deleted the perf/5093-element-shape-loop-clone branch August 8, 2026 00:18
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Audit before merge — verified, merged as v0.5.1350

The guard is verified in depth, and my two sabotages mapped its structure:

  1. Forcing the brand test always-true → exit 0, output still identical to
    node
    . The shape-id query behind it correctly declines the subclass
    receiver (no record for an ObjectHeader address), so this layer is
    redundant for that case — defense in depth doing its job. (The agent's
    SIGBUS-on-demand sabotage evidently cut deeper than mine did.)
  2. Forcing the query verdict true → 27 lines diverge at exit 127 — the
    fast clone running on non-conforming arrays produces wrong values and the
    gap test names them. That is the decisive layer, and it can fail.

Together: each layer is individually load-bearing for its own case, and no
single mutation of the guard survives the gap test.

One honest miss in my audit worth recording: my hand-rolled perf probe got
zero clone blocks (its shape didn't qualify — it was already served by an
older fast path) while still running fast, which would have been a vacuous
"perf confirmed" if I had only timed it. The IR census — element_shape.loop.*
block labels — is the discriminator, and the PR's 7 codegen tests assert it
across all four arms (fires for the #7480 shape, declines offset-index,
declines storing bodies, declines subclass element types, +0 for no-qualifying
programs). All 7 green here.

Root-dominance re-run (codegen change): corpus 129/129, --moving-only 0
violations with 40/40 seeded caught, --unrooted-allocas 0 over 7,860 allocas.
Full suites 1,886/0 + 684/0; all four lint gates + fmt clean.

The two design decisions that earn the merge:

The 6.2× keep[j].v shape is now at parity with node. The repsel route's
remaining step (element Ptr<Shape>, then object-literal elements) has its
prerequisite, its matrix (#7608), and its first consumer all merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant